package server;

import org.json.java.JSONArray;
import org.json.java.JSONException;
import org.json.java.JSONObject;

import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Created by IntelliJ IDEA.
 * User: ryan
 * Date: 12/1/11
 * Time: 12:15 PM
 * To change this template use File | Settings | File Templates.
 */
public class ChatServer implements Runnable{
    private ServerSocket socket;
    private final ExecutorService executorService = Executors.newCachedThreadPool();
    private volatile List<Socket> clientConnections;
    private volatile Set<String> clientNames;
    private final int port;
    private volatile boolean keepGoing=false;

    public void run() {
       Logger.getLogger(this.getClass().getName()).log(Level.INFO,"starting server");
       try{
            socket = new ServerSocket(port);
            keepGoing=true;

            while(keepGoing){
                final Socket clientSocket = socket.accept();
                clientSocket.setKeepAlive(true);
                ClientConnection clientConnection = new ClientConnection(clientSocket,this);
                executorService.submit(clientConnection);
            }
        }catch(IOException e){
            e.printStackTrace(System.err);
        }finally{
            try {
                if(socket!=null){
                    socket.close();
                }
            } catch (IOException e) {
                e.printStackTrace(System.err);
            }
            executorService.shutdown();
        }
    }

    public enum MESSAGE_TYPE{
        DISCONNECT,
        CONNECT,
        MESSAGE,
        CLIENTS
    }

    public ChatServer(int port){
        clientConnections = new LinkedList<Socket>();
        clientNames = new HashSet<String>();
        this.port=port;
    }

    /**
     * send a message to all the clients
     * needs to be synchronized because multiple connection threads will be writing to it
     * @param message
     */
    public synchronized void sendMessage(final JSONObject message){
        for(final Socket s :clientConnections){
            executorService.submit(new Runnable() {
                public void run() {
                    BufferedWriter out;
                    try{
                        Logger.getLogger(this.getClass().getName()).log(Level.INFO,"sending message "+message.toString());
                        out = new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));
                        out.write(message.toString());
                        out.write("\n");
                        out.flush();
                        Logger.getLogger(this.getClass().getName()).log(Level.INFO,"message sent");
                    }catch(IOException e){
                        e.printStackTrace(System.err);
                    }

                }
            });
        }
    }

    synchronized void removeClient(Socket s, String name){
        clientConnections.remove(s);
        clientNames.remove(name);
        System.out.println("CURRENT CLIENTS");
        for(String c: clientNames){
            System.out.print(c+",");
        }
        Logger.getLogger(this.getClass().getName()).log(Level.INFO,"sending client disconnect msg "+name);
        sendMessage(buildDisconnectMsg(name));
    }


    synchronized void  addClient(Socket s, String name){
        clientNames.add(name);
        sendMessage(buildConnectMsg(name));
        clientConnections.add(s);
    }

    public void stop(){
        Logger.getLogger(this.getClass().getName()).log(Level.INFO,"stopping server");
        keepGoing=false;
        try{
            if(socket!=null){
                socket.close();
            }
        }catch(IOException e){
            e.printStackTrace(System.err);
        }
    }

    public void start(){
        Thread t = new Thread(this);
        t.start();
    }

    private static JSONObject buildDisconnectMsg(String name) {
        JSONObject dc = new JSONObject();
        try{
            dc.put("type", MESSAGE_TYPE.DISCONNECT.toString());
            dc.put("name", name);
        }catch(JSONException e){
            e.printStackTrace(System.err);
        }
        return dc;
    }

    private static JSONObject buildConnectMsg(String name) {
        JSONObject dc = new JSONObject();
        try{
            dc.put("type",MESSAGE_TYPE.CONNECT.toString());
            dc.put("name", name);
        }catch(JSONException e){
            e.printStackTrace(System.err);
        }
        return dc;
    }

    void sentAllClientsMsg(Socket s){
        JSONObject j = new JSONObject();
        try{
            j.put("type", MESSAGE_TYPE.CLIENTS.toString());
            JSONArray arr = new JSONArray();
            for(String name: clientNames){
                Logger.getLogger(this.getClass().getName()).log(Level.INFO,"adding "+name+" to allClients");
                arr.put(name);
            }
            j.put("clients", arr);
            //need to synchronize just in case another message is being sent to this client at the same time
            synchronized (this){
                Logger.getLogger(this.getClass().getName()).log(Level.INFO,"writing allClients message: "+j.toString());
                BufferedWriter out = new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));
                out.write(j.toString());
                out.write("\n");
                out.flush();
            }
        }catch(JSONException e){
            e.printStackTrace(System.err);
        } catch (IOException e) {
            e.printStackTrace(System.err);
        }
    }

    public static void main(String[] args){
        int port;
        String usage = "Usage: java -jar ChatServer.jar [port]";
        if(args.length==0){
            System.out.println(usage);
            return;
        }else{
            try{
                port = Integer.parseInt(args[0]);
            }catch(NumberFormatException e){
                System.out.println("invalid port: "+args[0]);
                System.out.println(usage);
                return;
            }
        }
        ChatServer server = new ChatServer(port);
        server.start();
    }
}